﻿
using System;
using System.Collections.Generic;
using System.Linq;

namespace EmperialApps.WeatherSpark.Data {

    /// <summary>Contains helper methods for working with forecast data.</summary>
    public static class ForecastExtensions {

        /// <summary>Returns the symbol for temperature in the specified units.</summary>
        public static string GetTemperatureSymbol( this Units units ) {
            return units == Units.Metric ? "C" : "F";
        }

        /// <summary>Returns the symbol for pressure in the specified units.</summary>
        public static string GetPressureSymbol( this Units units ) {
            return units == Units.Metric ? "hPa" : "inHg";
        }

        /// <summary>Gets the low and high temperature extremes for the specified units.</summary>
        public static void GetExtremeTemperatures( this Units units, out double low, out double high ) {
            low = units == Units.Metric ? 0 : 30;
            high = units == Units.Metric ? 40 : 100;
        }


        /// <summary>Gets the name of the tile with the specified settings.</summary>
        public static string GetTileName( this Coordinate location, Units units, ForecastDisplayMode mode ) {
            string locationName = location.ToString( display: false );
            string unitsName = units.ToString( ).Substring( 0, 1 );
            string modeName = mode.ToString( ).Substring( 0, 1 );
            return string.Join( "_", locationName, unitsName, modeName );
        }


        /// <summary>Gets the <see cref="DateTimeOffset"/> for the <see cref="DateTimeOffset.Date"/> of the specified value.</summary>
        public static DateTimeOffset GetDateOffset( this DateTimeOffset value ) {
            return new DateTimeOffset( value.Date, value.Offset );
        }

        /// <summary>Calculates the number of hours from the first time to the second.</summary>
        public static double HoursFrom( this DateTimeOffset first, DateTimeOffset second ) {
            TimeSpan difference = first - second;
            return difference.TotalHours;
        }

        /// <summary>Gets the end <see cref="DateTimeOffset"/> for the specified forecast.</summary>
        public static DateTimeOffset End( this Forecast forecast ) {
            int hours = forecast.Temperature.Count * forecast.Interval;
            return forecast.Start.AddHours( hours );
        }


        /// <summary>Updates the specified data values to represent the forecast temperature in the given units.</summary>
        public static DataValues GetTemperatureValues( this Forecast forecast, Units units, ref DataValues cachedValues ) {
            return GetCachedValues( forecast.Temperature, units, ConvertValue.ToFahrenheit, ref cachedValues );
        }

        /// <summary>Updates the specified data values to represent the forecast wind speed in the given units.</summary>
        public static DataValues GetWindSpeedValues( this Forecast forecast, Units units, ref DataValues cachedValues ) {
            return GetCachedValues( forecast.WindSpeed, units, ConvertValue.ToMilesPerHour, ref cachedValues );
        }

        /// <summary>Updates the specified data values to represent the forecast pressure in the given units.</summary>
        public static DataValues GetPressureValues( this Forecast forecast, Units units, ref DataValues cachedValues ) {
            return GetCachedValues( forecast.Pressure, units, ConvertValue.ToInchesOfMercury, ref cachedValues );
        }

        /// <summary>Returns the linear interpolation of the value at the specified offset in the collection.</summary>
        public static double GetInterpolatedValue( this DataValues values, double offset, Func<double, double> convert, double modulo = 100000 ) {
            int maximumIndex = values.Count - 1;
            if( maximumIndex < 0 )
                return 0.0;

            double adjustedOffset = offset / values.Interval;
            int index = Math.Min( maximumIndex, Math.Max( 0, (int)adjustedOffset ) );
            int nextIndex = Math.Min( maximumIndex, index + 1 );

            double first = values[index];
            double second = values[nextIndex];
            double difference = first - second;
            if( Math.Abs( difference ) > modulo / 2 ) {
                if( difference < 0 )
                    first += modulo;
                else
                    second += modulo;
            }

            double interpolationOffset = adjustedOffset - index;
            double currentValue =
                  first * (1 - interpolationOffset)
                + second * interpolationOffset;

            for( int i = index + 1; double.IsNaN( currentValue ) && i < values.Count; ++i )
                currentValue = values[i];

            return convert( currentValue % modulo );
        }

        /// <summary>Returns the one-day low and high values for the specified date, if there is enough data available.</summary>
        public static Pair<double, double>? TryGetExtremeValues( this DataValues values, DateTimeOffset start, DateTimeOffset offset, int minimumCount = 16 ) {
            DateTimeOffset date = offset.GetDateOffset( );
            int valuesCount = values.Count * values.Interval;
            int dateIndex, dateCount;

            // If given date comes after start date, use all date for the date (up to end of values collection).
            if( start < date ) {
                dateIndex = (int)date.HoursFrom( start );

                double desiredCount = date.AddDays( 1 ).HoursFrom( date );
                double availableCount = start.AddHours( valuesCount ).HoursFrom( date );
                dateCount = (int)Math.Min( desiredCount, availableCount );
            }
            // Otherwise, use available data from start of values collection to the next date.
            else {
                dateIndex = 0;
                dateCount = (int)date.AddDays( 1 ).HoursFrom( start );
            }

            // If we do not have enough data to determine the low and high values for the date, return failure.
            int index = dateIndex / values.Interval;
            int count = 1 + dateCount / values.Interval;
            if( dateCount <= minimumCount || double.IsNaN( values.Skip( index ).FirstOrDefault( ) ) )
                return null;

            // Otherwise, find the low an high values in the range of date values.
            double low = double.PositiveInfinity;
            double high = double.NegativeInfinity;
            var dateValues = values.Skip( index ).Take( count );
            foreach( double value in dateValues ) {
                low = Math.Min( value, low );
                high = Math.Max( value, high );
            }

            return Pair.Create( low, high );
        }


        #region Private Members

        private static DataValues GetCachedValues( DataValues values, Units units, Func<double, double> toImperial, ref DataValues cachedValues ) {
            if( units == Units.Metric )
                cachedValues = values;
            else if( !ConvertedDataValues.IsMatch( cachedValues, values ) )
                cachedValues = new ConvertedDataValues( values, values.Select( toImperial ).ToArray( ) );

            return cachedValues;
        }


        private sealed class ConvertedDataValues : DataValues {
            private readonly DataValues _originalValues;

            public ConvertedDataValues( DataValues originalValues, IList<double> convertedValues )
                : base( convertedValues, originalValues.Interval ) {
                this._originalValues = originalValues;
            }

            public static bool IsMatch( DataValues values, DataValues originalValues ) {
                var convertedValues = values as ConvertedDataValues;
                return convertedValues != null
                    && object.ReferenceEquals( convertedValues._originalValues, originalValues );
            }
        }

        #endregion
    }

}
